package com.hcss.common.util.excel.util;

import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.EncryptionMode;
import org.apache.poi.poifs.crypt.Encryptor;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.usermodel.*;

import java.io.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Description Excel 写入器<br>
 * 此工具用于通过POI将数据写出到Excel<br>
 * 可以自动选择03与07Excel格式<br>
 * 可以选择是否对Excel文件进行加密
 * @Author:momo
 * @Since:2018年12月21日
 * @Version:1.1.0
 */
public class ExcelWriter implements Closeable {
	
	// 工作簿
	private Workbook workbook;
	// 当前sheet
	private Sheet sheet;
	// 目标文件
	private File destFile;
	// 当前行
	private AtomicInteger currentRow = new AtomicInteger(0);
	// 标题别名
	private Map<String, String> headerAlias;
	// 密码
	private String password;
	//
	private OutputStream os = null;

	/**
	 * 写入Excel文件
	 * @param destFile 写入文件地址
	 */
	public ExcelWriter(String destFile) {
		this(new File(destFile));
	}
	
	/**
	 * 写入Excel文件
	 * @param destFile
	 */
	public ExcelWriter(File destFile) {
		this(destFile,"sheet1");
	}
	
	/**
	 * 写入Excel文件
	 * @param destFile 目的地址
	 * @param sheetName sheet名称
	 */
	public ExcelWriter(File destFile, String sheetName) {
		this(destFile, sheetName, null);
	}
	
	/**
	 * 写入Excel文件
	 * @param destFile
	 * @param sheetName
	 * @param password
	 */
	public ExcelWriter(File destFile, String sheetName, String password) {
		if(destFile.exists()) {
			String filename = getRandomFileName(destFile.getName());
			destFile = new File(destFile.getParentFile(), filename);
			// 如果原文件存在的话，这里暂定的策略是文件名后边加时间戳+随机符
		}
		boolean isXlsx = ExcelFileUtil.endWith(destFile.getName(), ".xlsx", true);
		workbook = WorkbookUtil.createBook(isXlsx, password);
		sheetName = ExcelFileUtil.strIsEmpty(sheetName) ? "sheet1" : sheetName;
		sheet = workbook.createSheet(sheetName);
		this.destFile = destFile;
		this.password = password;
		this.destFile = destFile;
	}
	
	public ExcelWriter(OutputStream os,boolean isXlsx,String sheetName, String password) {
		this.os = os;
		workbook = WorkbookUtil.createBook(isXlsx, password);
		sheetName = ExcelFileUtil.strIsEmpty(sheetName) ? "sheet1" : sheetName;
		sheet = workbook.createSheet(sheetName);
	}
	
	/**
	 * 重新获取文件名称
	 * @param fileName
	 * @return
	 */
	public String getRandomFileName(String fileName) {
		Integer index = fileName.lastIndexOf(".");
		String prefix = fileName.substring(0, index);
		String suffix = fileName.substring(index );
		
		Random random = new Random();
		Long time = System.currentTimeMillis();
		String result="";
		for (int i=0;i<6;i++){
			result+=random.nextInt(10);
		}
		
		return prefix + "-" + time + "-" + result + suffix;
	}
	
	/**
	 * 写出数据，本方法只是将数据写入Workbook中的Sheet，并不写出到文件
	 * 写出的起始行为当前行号，根据写出的的行数，当前行号自动增加
	 * data中元素支持的类型有：
	 * 
	 * 1. Iterable，既元素为一个集合，元素被当作一行，data表示多行
	 * 2. Map，既元素为一个Map，第一个Map的keys作为首行，剩下的行为Map的values，data表示多行 
	 * 3. Bean，既元素为一个Bean，第一个Bean的字段名列表会作为首行，剩下的行为Bean的字段值列表，data表示多行 
	 * 4. 无法识别，不输出
	 * 
	 * @param data 数据
	 * @return this
	 */
	public ExcelWriter write(Iterable<?> data) {
		int index = 0;
		for (Object object : data) {
			if (object instanceof Iterable) {
				// 普通多行数据
				writeRow((Iterable<?>) object);
			} else if (object instanceof Map) {
				// Map表示一行，第一条数据的key做为标题行
				writeRows((Map<?, ?>) object, 0 == index);
			} else if (ExcelBeanUtil.isBean(object.getClass())) {
				// 一个Bean对象表示一行
				writeRows(ExcelBeanUtil.beanToMap(object,headerAlias.keySet()), 0 == index);
			} else {
				break;
			}
			index++;
		}
		if (0 == index) {
			// 在无法识别元素类型的情况下，做为一行对待
			writeRow(data);
		}
		return this;
	}
	
	/**
	 * 写出一行数据
	 * 本方法只是将数据写入Workbook中的Sheet，并不写出到文件
	 * @param rowData 一行的数据
	 * @return this
	 */
	public ExcelWriter writeRow(Iterable<?> rowData) {
		writeRow(this.sheet.createRow(this.currentRow.getAndIncrement()), rowData, false);
		return this;
	}
	
	/**
	 * 将一个Map写入到Excel，isWriteKeys为true写出两行，Map的keys做为一行，values做为第二行，否则只写出一行values
	 * 
	 * @param rowMap 写出的Map
	 * @param isWriteKeys 为true写出两行，Map的keys做为一行，values做为第二行，否则只写出一行values
	 * @return this
	 */
	public ExcelWriter writeRows(Map<?, ?> rowMap, boolean isWriteKeys) {
		if (isWriteKeys) {
			writeHeadRow(aliasHeader(rowMap.keySet()));
		}
		writeRow(rowMap.values());
		return this;
	}
	
	/**
	 * 写出一行标题数据
	 * 本方法只是将数据写入Workbook中的Sheet，并不写出到文件
	 * 写出的起始行为当前行号，根据写出的的行数，当前行号自动+1
	 * 
	 * @param rowData 一行的数据
	 * @return this
	 */
	public ExcelWriter writeHeadRow(Iterable<?> rowData) {
		writeRow(this.sheet.createRow(this.currentRow.getAndIncrement()), rowData,  true);
		return this;
	}
	
	/**
	 * 写一行数据
	 * 
	 * @param row 行
	 * @param rowData 一行的数据
	 * @param isHeader 是否为标题行
	 */
	public static void writeRow(Row row, Iterable<?> rowData , boolean isHeader) {
		int i = 0;
		Cell cell;
		for (Object value : rowData) {
			cell = row.createCell(i);
			setCellValue(cell, value, isHeader);
			i++;
		}//[航班日期, 航段, 代理人, 代理人编码, 运单号前缀, 运单单号, 品名, 货物类型, 重量]
	}
	
	/**
	 * 设置单元格值
	 * 根据传入的styleSet自动匹配样式
	 * 当为头部样式时默认赋值头部样式，但是头部中如果有数字、日期等类型，将按照数字、日期样式设置
	 */
	public static void setCellValue(Cell cell, Object value,  boolean isHeader) {
		if (null == value) {
			cell.setCellValue("");
		} else if (value instanceof Date) {
			cell.setCellValue((Date) value);
		} else if (value instanceof Calendar) {
			cell.setCellValue((Calendar) value);
		} else if (value instanceof Boolean) {
			cell.setCellValue((Boolean) value);
		} else if (value instanceof RichTextString) {
			cell.setCellValue((RichTextString) value);
		} else if (value instanceof Number) {
			cell.setCellValue(((Number) value).doubleValue());
		} else {
			cell.setCellValue(value.toString());
		}
	}
	
	/**
	 * 将Excel Workbook刷出到预定义的文件
	 * 如果用户未自定义输出的文件，将抛出异常
	 */
	public ExcelWriter flush() throws IOException {
		return flush(this.destFile);
	}
	
	/**
	 * 将Excel Workbook刷出到文件
	 */
	public ExcelWriter flush(File destFile) throws IOException {
		OutputStream out = null;
		try {
			if(os != null) {
				out = os;
			}else {
				out = ExcelFileUtil.getOutputStream(destFile);
			}
			flush(out);
		} finally {
			out.close();
		}
		return this;
	}
	
	/**
	 * 将Excel Workbook刷出到输出流
	 */
	public ExcelWriter flush(OutputStream out) {
		try {
			this.workbook.write(out);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return this;
	}
	
	/**
	 * 关闭工作簿
	 * 如果用户设定了目标文件，先写出目标文件后给关闭工作簿
	 * @throws IOException 
	 */
	@Override
	public void close() throws IOException{
		flush();
		workbook.close();
		try{
			if(WorkbookUtil.getNeedEncrypt() && !WorkbookUtil.getIsEncrypt()) {
				// 07 方式在此加密
				final POIFSFileSystem fs = new POIFSFileSystem();
				final EncryptionInfo info = new EncryptionInfo(EncryptionMode.standard);
				final Encryptor enc = info.getEncryptor();
				 
				enc.confirmPassword(password);
				 
				final OPCPackage opc = OPCPackage.open(destFile, PackageAccess.READ_WRITE);
				final OutputStream os = enc.getDataStream(fs);
				opc.save(os);
				opc.close();
				 
				final FileOutputStream fos = new FileOutputStream(destFile);
				fs.writeFilesystem(fos);
				fos.close();
			}
		}catch(Exception e) {
			System.err.println("加密失败......");
		}
		// 清空对象
		this.currentRow = null;
		this.sheet = null;
		this.workbook = null;
		this.os = null;
	}
	
	/**
	 * 设置标题别名，key为Map中的key，value为别名
	 * @param headerAlias 标题别名
	 */
	public ExcelWriter setHeaderAlias(Map<String, String> headerAlias) {
		this.headerAlias = headerAlias;
		return this;
	}
	
	/**
	 * 为指定的key列表添加标题别名，如果没有定义key的别名，使用原key
	 * 
	 * @param keys 键列表
	 * @return 别名列表
	 */
	private Collection<?> aliasHeader(Collection<?> keys) {
		if (this.headerAlias.isEmpty()) {
			return keys;
		}
		final List<Object> alias = new ArrayList<>();
		String aliasName;
		for (Object key : keys) {
			aliasName = this.headerAlias.get(key);
			alias.add(null == aliasName ? key : aliasName);
		}
		return alias;
	}
	
}
